/*
 * $Id: session.c 1003 2008-11-20 19:24:48Z jasta00 $
 *
 * Copyright (C) 2008 Josh Guilfoyle <jasta@devtcg.org>
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the
 * Free Software Foundation; either version 2, or (at your option) any
 * later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 */

#include "main.h"

#include <libsyncml/syncml.h>
#include <libsyncml/sml_ds_server.h>

#include "db.h"
#include "session.h"
#include "item.h"

/*****************************************************************************/

FiveSession *five_session_new(SmlDsSession *dsess)
{
	FiveSession *fsess;
	sqlite3_stmt *stmt;
	const char *sql;
	int rc;

	assert(dsess != NULL);

	fsess = g_malloc0(sizeof(FiveSession));
	fsess->dsession = dsess;

	if (db_open_again(&fsess->dbconn) == FALSE)
	{
		g_error("Failed to open database connection");
		g_free(fsess);
		return NULL;
	}

	sql =
	  "SELECT last, timestamp "
	  "  FROM sync_events "
	  " WHERE src = ?";

	rc = sqlite3_prepare(fsess->dbconn, sql, -1, &stmt, NULL);
	assert(rc == SQLITE_OK);

	sqlite3_bind_text(stmt, 1, five_session_get_peer_ident(fsess), -1, NULL);

	if ((rc = sqlite3_step(stmt)) == SQLITE_ROW)
	{
		fsess->client.last = (time_t)sqlite3_column_int64(stmt, 0);
		fsess->server.last = (time_t)sqlite3_column_int64(stmt, 1);
	}
	else
	{
		fsess->client.last = 0;
		fsess->server.last = 0;
	}

	sqlite3_finalize(stmt);

	return fsess;
}

void five_session_free(FiveSession *fsess)
{
	db_close_again(fsess->dbconn);
	g_free(fsess);
}

/*****************************************************************************/

const char *five_session_get_peer_ident(FiveSession *fsess)
{
	SmlLocation *loc;
	const char *src;

	assert(fsess != NULL);

	loc = smlSessionGetTarget(smlDsSessionGetSession(fsess->dsession));
	assert(loc != NULL);

	src = smlLocationGetURI(loc);
	assert(src != NULL);

	return src;
}

/*****************************************************************************/

gboolean five_session_item_map(FiveSession *fsess, const char *mime,
  sqlite_int64 guid, sqlite_int64 luid)
{
	sqlite3_stmt *stmt;
	const char *domain;
	const char *sql;
	int rc;

	/* TODO: Optimize. */
	if (strcmp(mime, "application/x-fivedb-artist") == 0)
		domain = "artist";
	else if (strcmp(mime, "application/x-fivedb-album") == 0)
		domain = "album";
	else if (strcmp(mime, "application/x-fivedb-song") == 0)
		domain = "song";
	else if (strcmp(mime, "application/x-fivedb-playlist") == 0)
		domain = "playlist";
	else
	{
		g_warning("Unknown mime type in <Add> reply: %s", mime);
		return FALSE;
	}

	sql =
	  "REPLACE INTO item_map (src, domain, guid, luid) VALUES (?, ?, ?, ?)";

	rc = sqlite3_prepare(fsess->dbconn, sql, -1, &stmt, NULL);
	assert(rc == SQLITE_OK);

	sqlite3_bind_text(stmt, 1, five_session_get_peer_ident(fsess), -1, SQLITE_STATIC);
	sqlite3_bind_text(stmt, 2, domain, -1, SQLITE_STATIC);
	sqlite3_bind_int64(stmt, 3, guid);
	sqlite3_bind_int64(stmt, 4, luid);

	if ((rc = sqlite3_step(stmt)) != SQLITE_DONE)
	{
		g_critical("Failed to create item map");
		sqlite3_finalize(stmt);
		return FALSE;
	}

	sqlite3_finalize(stmt);
	return TRUE;
}

gboolean five_session_wipe_item_map(FiveSession *fsess)
{
	sqlite3_stmt *stmt;
	int rc;
	gboolean ret;

	rc = sqlite3_prepare(fsess->dbconn, "DELETE FROM item_map WHERE src = ?", -1, &stmt, NULL);
	assert(rc == SQLITE_OK);

	sqlite3_bind_text(stmt, 1, five_session_get_peer_ident(fsess), -1, SQLITE_STATIC);

	if ((rc = sqlite3_step(stmt)) == SQLITE_DONE)
		ret = TRUE;
	else
	{
		g_critical("Failed to clear item map");
		ret = FALSE;
	}

	sqlite3_finalize(stmt);

	return ret;
}

gboolean five_session_update_anchors(FiveSession *fsess)
{
	sqlite3_stmt *stmt;
	const char *sql;
	int rc;

	if (fsess->server.last == 0)
		sql = "INSERT INTO sync_events (last, timestamp, src) VALUES (?, ?, ?)";
	else
		sql = "UPDATE sync_events SET last = ?, timestamp = ? WHERE src = ?";

	rc = sqlite3_prepare(fsess->dbconn, sql, -1, &stmt, NULL);
	assert(rc == SQLITE_OK);

	sqlite3_bind_int64(stmt, 1, (sqlite_int64)fsess->client.next);
	sqlite3_bind_int64(stmt, 2, (sqlite_int64)fsess->server.next);
	sqlite3_bind_text(stmt, 3, five_session_get_peer_ident(fsess), -1, NULL);

	if ((rc = sqlite3_step(stmt)) != SQLITE_DONE)
		g_critical("Error updating sync_events: %s", sqlite3_errmsg(fsess->dbconn));

	sqlite3_finalize(stmt);

	if (rc != SQLITE_DONE)
		return FALSE;

	return TRUE;
}

/*****************************************************************************/

static unsigned int add_dump_artists(FiveSession *fsess, GList **chglist)
{
	sqlite3_stmt *stmt;
	const char *sql;
	unsigned int len = 0;
	int rc;

	sql =
		"   SELECT * "
		"     FROM artists "
		"    WHERE pending_delete = 0";

	rc = sqlite3_prepare(fsess->dbconn, sql, -1, &stmt, NULL);
	assert(rc == SQLITE_OK);

	while ((rc = sqlite3_step(stmt)) == SQLITE_ROW)
	{
		FiveChange *chg = g_new0(FiveChange, 1);
		GString *data;

		chg->type = SML_CHANGE_ADD;
		chg->guid = sqlite3_column_int64(stmt, 0);
		chg->luid = -1;
		chg->mime = "application/x-fivedb-artist";

		item_set_artist_data(chg, stmt, 1);

		*chglist = g_list_append(*chglist, chg);
		len++;
	}

	if (rc != SQLITE_DONE)
		g_critical("sqlite3_step returned %d: %s\n", rc, sqlite3_errmsg(fsess->dbconn));

	sqlite3_finalize(stmt);

	return len;
}

static unsigned int add_dump_albums(FiveSession *fsess, GList **chglist)
{
	sqlite3_stmt *stmt;
	const char *sql;
	unsigned int len = 0;
	int rc;

	sql =
		"   SELECT * "
		"     FROM albums "
		"    WHERE pending_delete = 0";

	rc = sqlite3_prepare(fsess->dbconn, sql, -1, &stmt, NULL);
	assert(rc == SQLITE_OK);

	while ((rc = sqlite3_step(stmt)) == SQLITE_ROW)
	{
		FiveChange *chg = g_new0(FiveChange, 1);
		GString *data;

		chg->type = SML_CHANGE_ADD;
		chg->guid = sqlite3_column_int64(stmt, 0);
		chg->luid = -1;
		chg->mime = "application/x-fivedb-album";

		item_set_album_data(chg, -1, stmt, 1);

		*chglist = g_list_append(*chglist, chg);
		len++;
	}

	if (rc != SQLITE_DONE)
		g_critical("sqlite3_step returned %d: %s\n", rc, sqlite3_errmsg(fsess->dbconn));

	sqlite3_finalize(stmt);

	return len;
}

static unsigned int add_dump_songs(FiveSession *fsess, GList **chglist)
{
	sqlite3_stmt *stmt;
	const char *sql;
	unsigned int len = 0;
	int rc;

	sql =
		"   SELECT _id, title, artist_id, album_id, length, track, genre, "
		"          discovery_date, size, mimetype, bitrate "
		"     FROM songs "
		"    WHERE pending_delete = 0";

	rc = sqlite3_prepare(fsess->dbconn, sql, -1, &stmt, NULL);
	assert(rc == SQLITE_OK);

	while ((rc = sqlite3_step(stmt)) == SQLITE_ROW)
	{
		FiveChange *chg = g_new0(FiveChange, 1);
		GString *data;

		chg->type = SML_CHANGE_ADD;
		chg->guid = sqlite3_column_int64(stmt, 0);
		chg->luid = -1;
		chg->mime = "application/x-fivedb-song";

		item_set_song_data(chg, -1, -1, stmt, 0);

		*chglist = g_list_append(*chglist, chg);
		len++;
	}

	if (rc != SQLITE_DONE)
		g_critical("sqlite3_step returned %d: %s\n", rc, sqlite3_errmsg(fsess->dbconn));

	sqlite3_finalize(stmt);

	return len;
}

static void set_playlist_data_helper(FiveSession *fsess, FiveChange *chg,
  sqlite3_stmt *stmt, int idcol)
{
	sqlite_int64 playlist_id;
	sqlite3_stmt *songs_stmt;
	const char *sql;
	int rc;

	playlist_id = sqlite3_column_int64(stmt, idcol);
	assert(playlist_id >= 0);

	sql =
	  "   SELECT m.luid AS song_luid, ps.song_id AS song_guid "
	  "     FROM playlist_songs ps "
	  "LEFT JOIN item_map m ON m.guid = ps.song_id AND m.domain = 'song' AND m.src = ?"
	  "    WHERE playlist_id = ? "
	  " ORDER BY position ASC";

	rc = sqlite3_prepare(fsess->dbconn, sql, -1, &songs_stmt, NULL);
	assert(rc == SQLITE_OK);

	sqlite3_bind_text(songs_stmt, 1, five_session_get_peer_ident(fsess), -1, SQLITE_STATIC);
	sqlite3_bind_int64(songs_stmt, 2, playlist_id);

	item_set_playlist_data(chg, stmt, songs_stmt, idcol);

	sqlite3_finalize(songs_stmt);
}

static unsigned int add_dump_playlists(FiveSession *fsess, GList **chglist)
{
	sqlite3_stmt *stmt;
	const char *sql;
	unsigned int len = 0;
	int rc;

	sql =
		"   SELECT _id, name, created_date "
		"     FROM playlists "
		"    WHERE pending_delete = 0";

	rc = sqlite3_prepare(fsess->dbconn, sql, -1, &stmt, NULL);
	assert(rc == SQLITE_OK);

	while ((rc = sqlite3_step(stmt)) == SQLITE_ROW)
	{
		FiveChange *chg = g_new0(FiveChange, 1);
		GString *data;

		chg->type = SML_CHANGE_ADD;
		chg->guid = sqlite3_column_int64(stmt, 0);
		chg->luid = -1;
		chg->mime = "application/x-fivedb-playlist";

		set_playlist_data_helper(fsess, chg, stmt, 0);

		*chglist = g_list_append(*chglist, chg);
		len++;
	}

	if (rc != SQLITE_DONE)
		g_critical("sqlite3_step returned %d: %s\n", rc, sqlite3_errmsg(fsess->dbconn));

	sqlite3_finalize(stmt);

	return len;
}

/*****************************************************************************/

static unsigned int add_changes_artists(FiveSession *fsess, GList **chglist,
  time_t lastAnchor)
{
	sqlite3_stmt *stmt;
	const char *sql;
	unsigned int len = 0;
	int rc;

	sql =
	  "   SELECT c.event, c.item_id, m.luid, i.pending_delete, i.* "
	  "     FROM item_changelog c "
	  "LEFT JOIN artists i ON i._id = c.item_id "
	  "LEFT JOIN item_map m ON m.guid = c.item_id AND m.domain = ? AND m.src = ?"
	  "    WHERE c.domain = ? "
	  "      AND c.timestamp > ? "
	  " ORDER BY c.timestamp ASC, c._id ASC ";

	rc = sqlite3_prepare(fsess->dbconn, sql, -1, &stmt, NULL);
	assert(rc == SQLITE_OK);

	sqlite3_bind_text(stmt, 1, "artist", -1, SQLITE_STATIC);
	sqlite3_bind_text(stmt, 2, five_session_get_peer_ident(fsess), -1, SQLITE_STATIC);
	sqlite3_bind_text(stmt, 3, "artist", -1, SQLITE_STATIC);
	sqlite3_bind_int64(stmt, 4, (sqlite_int64)lastAnchor);

	while ((rc = sqlite3_step(stmt)) == SQLITE_ROW)
	{
		int type;
		sqlite_int64 guid;
		sqlite_int64 luid;
		gboolean pending;
		FiveChange *chg;

		type = sqlite3_column_int(stmt, 0);
		guid = sqlite3_column_int64(stmt, 1);

		/* Guess that an Add command for a deleted item will also have a
		 * Delete command somewhere later in this changelog.  Do nothing. */
		pending = (sqlite3_column_int(stmt, 3) == 1) ? TRUE : FALSE;
		if (type == SML_CHANGE_ADD && pending == TRUE)
			continue;

		if (sqlite3_column_type(stmt, 2) != SQLITE_NULL)
			luid = sqlite3_column_int64(stmt, 2);
		else
		{
			/* If there is no mapping to this entry, then we can't communicate
			 * anything meaningful about it.  This is special logic to
			 * efficiently collapse multiple changes between syncs (imagine an
			 * Add, Replace, Delete change set for the same item).  In this
			 * case, the Add will be ignored by logic above concerning
			 * `pending_delete' and Replace/Delete will be ignored because they
			 * have no matching item mapping. */
			if (type != SML_CHANGE_ADD)
				continue;

			luid = -1;
		}

		chg = g_new0(FiveChange, 1);
		assert(chg != NULL);

		chg->type = type;
		chg->guid = guid;
		chg->luid = luid;

		chg->mime = "application/x-fivedb-artist";

		if (chg->type != SML_CHANGE_DELETE)
			item_set_artist_data(chg, stmt, 4);
		else
		{
			chg->datasz = 0;
			chg->data = NULL;
		}

		*chglist = g_list_append(*chglist, chg);
		len++;
	}

	if (rc != SQLITE_DONE)
		g_critical("sqlite3_step returned %d: %s\n", rc, sqlite3_errmsg(fsess->dbconn));

	sqlite3_finalize(stmt);

	return len;
}

static unsigned int add_changes_albums(FiveSession *fsess, GList **chglist,
  time_t lastAnchor)
{
	sqlite3_stmt *stmt;
	const char *sql;
	unsigned int len = 0;
	int rc;

	sql =
	  "   SELECT c.event, c.item_id, m.luid, "
	  "          martist.luid AS artist_luid, i.pending_delete, i.* "
	  "     FROM item_changelog c "
	  "LEFT JOIN albums i ON i._id = c.item_id "
	  "LEFT JOIN item_map m ON m.guid = c.item_id AND m.domain = ? AND m.src = ?"
	  "LEFT JOIN item_map martist ON martist.guid = i.artist_id AND martist.domain = 'artist' AND martist.src = ?"
	  "    WHERE c.domain = ? "
	  "      AND c.timestamp > ? "
	  " ORDER BY c.timestamp ASC, c._id ASC ";

	rc = sqlite3_prepare(fsess->dbconn, sql, -1, &stmt, NULL);
	assert(rc == SQLITE_OK);

	sqlite3_bind_text(stmt, 1, "album", -1, SQLITE_STATIC);
	sqlite3_bind_text(stmt, 2, five_session_get_peer_ident(fsess), -1, SQLITE_STATIC);
	sqlite3_bind_text(stmt, 3, five_session_get_peer_ident(fsess), -1, SQLITE_STATIC);
	sqlite3_bind_text(stmt, 4, "album", -1, SQLITE_STATIC);
	sqlite3_bind_int64(stmt, 5, (sqlite_int64)lastAnchor);

	while ((rc = sqlite3_step(stmt)) == SQLITE_ROW)
	{
		int type;
		sqlite_int64 guid;
		sqlite_int64 luid;
		gboolean pending;
		FiveChange *chg;

		type = sqlite3_column_int(stmt, 0);
		guid = sqlite3_column_int64(stmt, 1);

		pending = (sqlite3_column_int(stmt, 4) == 1) ? TRUE : FALSE;
		if (type == SML_CHANGE_ADD && pending == TRUE)
			continue;

		if (sqlite3_column_type(stmt, 2) != SQLITE_NULL)
			luid = sqlite3_column_int64(stmt, 2);
		else
		{
			if (type != SML_CHANGE_ADD)
				continue;

			luid = -1;
		}

		chg = g_new0(FiveChange, 1);
		assert(chg != NULL);

		chg->type = type;
		chg->guid = guid;
		chg->luid = luid;

		chg->mime = "application/x-fivedb-album";

		if (chg->type != SML_CHANGE_DELETE)
			item_set_album_data(chg, 3, stmt, 5);
		else
		{
			chg->datasz = 0;
			chg->data = NULL;
		}

		*chglist = g_list_append(*chglist, chg);
		len++;
	}

	if (rc != SQLITE_DONE)
		g_critical("sqlite3_step returned %d: %s\n", rc, sqlite3_errmsg(fsess->dbconn));

	sqlite3_finalize(stmt);

	return len;
}

static unsigned int add_changes_songs(FiveSession *fsess, GList **chglist,
  time_t lastAnchor)
{
	sqlite3_stmt *stmt;
	const char *sql;
	unsigned int len = 0;
	int rc;

	sql =
	  "   SELECT c.event, c.item_id, m.luid, "
	  "          martist.luid AS artist_luid, malbum.luid AS album_luid, "
	  "          i.pending_delete, i.* "
	  "     FROM item_changelog c "
	  "LEFT JOIN songs i ON i._id = c.item_id "
	  "LEFT JOIN item_map m ON m.guid = c.item_id AND m.domain = ? AND m.src = ?"
	  "LEFT JOIN item_map martist ON martist.guid = i.artist_id AND martist.domain = 'artist' AND martist.src = ?"
	  "LEFT JOIN item_map malbum ON malbum.guid = i.album_id AND malbum.domain = 'album' AND malbum.src = ?"
	  "    WHERE c.domain = ? "
	  "      AND c.timestamp > ? "
	  " ORDER BY c.timestamp ASC, c._id ASC ";

	rc = sqlite3_prepare(fsess->dbconn, sql, -1, &stmt, NULL);
	assert(rc == SQLITE_OK);

	sqlite3_bind_text(stmt, 1, "song", -1, SQLITE_STATIC);
	sqlite3_bind_text(stmt, 2, five_session_get_peer_ident(fsess), -1, SQLITE_STATIC);
	sqlite3_bind_text(stmt, 3, five_session_get_peer_ident(fsess), -1, SQLITE_STATIC);
	sqlite3_bind_text(stmt, 4, five_session_get_peer_ident(fsess), -1, SQLITE_STATIC);
	sqlite3_bind_text(stmt, 5, "song", -1, SQLITE_STATIC);
	sqlite3_bind_int64(stmt, 6, (sqlite_int64)lastAnchor);

	while ((rc = sqlite3_step(stmt)) == SQLITE_ROW)
	{
		int type;
		sqlite_int64 guid;
		sqlite_int64 luid;
		gboolean pending;
		FiveChange *chg;

		type = sqlite3_column_int(stmt, 0);
		guid = sqlite3_column_int64(stmt, 1);

		pending = (sqlite3_column_int(stmt, 5) == 1) ? TRUE : FALSE;
		if (type == SML_CHANGE_ADD && pending == TRUE)
			continue;

		if (sqlite3_column_type(stmt, 2) != SQLITE_NULL)
			luid = sqlite3_column_int64(stmt, 2);
		else
		{
			if (type != SML_CHANGE_ADD)
				continue;

			luid = -1;
		}

		chg = g_new0(FiveChange, 1);
		assert(chg != NULL);

		chg->type = type;
		chg->guid = guid;
		chg->luid = luid;

		chg->mime = "application/x-fivedb-song";

		if (chg->type != SML_CHANGE_DELETE)
			item_set_song_data(chg, 3, 4, stmt, 6);
		else
		{
			chg->datasz = 0;
			chg->data = NULL;
		}

		*chglist = g_list_append(*chglist, chg);
		len++;
	}

	if (rc != SQLITE_DONE)
		g_critical("sqlite3_step returned %d: %s\n", rc, sqlite3_errmsg(fsess->dbconn));

	sqlite3_finalize(stmt);

	return len;
}

static unsigned int add_changes_playlists(FiveSession *fsess, GList **chglist,
  time_t lastAnchor)
{
	sqlite3_stmt *stmt;
	const char *sql;
	unsigned int len = 0;
	int rc;

	sql =
	  "   SELECT c.event, c.item_id, m.luid, "
	  "          i.pending_delete, i.* "
	  "     FROM item_changelog c "
	  "LEFT JOIN playlists i ON i._id = c.item_id "
	  "LEFT JOIN item_map m ON m.guid = c.item_id AND m.domain = ? AND m.src = ?"
	  "    WHERE c.domain = ? "
	  "      AND c.timestamp > ? "
	  " ORDER BY c.timestamp ASC, c._id ASC ";

	rc = sqlite3_prepare(fsess->dbconn, sql, -1, &stmt, NULL);
	assert(rc == SQLITE_OK);

	sqlite3_bind_text(stmt, 1, "playlist", -1, SQLITE_STATIC);
	sqlite3_bind_text(stmt, 2, five_session_get_peer_ident(fsess), -1, SQLITE_STATIC);
	sqlite3_bind_text(stmt, 3, "playlist", -1, SQLITE_STATIC);
	sqlite3_bind_int64(stmt, 4, (sqlite_int64)lastAnchor);

	while ((rc = sqlite3_step(stmt)) == SQLITE_ROW)
	{
		int type;
		sqlite_int64 guid;
		sqlite_int64 luid;
		gboolean pending;
		FiveChange *chg;

		type = sqlite3_column_int(stmt, 0);
		guid = sqlite3_column_int64(stmt, 1);

		pending = (sqlite3_column_int(stmt, 3) == 1) ? TRUE : FALSE;
		if (type == SML_CHANGE_ADD && pending == TRUE)
			continue;

		if (sqlite3_column_type(stmt, 2) != SQLITE_NULL)
			luid = sqlite3_column_int64(stmt, 2);
		else
		{
			if (type != SML_CHANGE_ADD)
				continue;

			luid = -1;
		}

		chg = g_new0(FiveChange, 1);
		assert(chg != NULL);

		chg->type = type;
		chg->guid = guid;
		chg->luid = luid;

		chg->mime = "application/x-fivedb-playlist";

		if (chg->type != SML_CHANGE_DELETE)
			set_playlist_data_helper(fsess, chg, stmt, 4);
		else
		{
			chg->datasz = 0;
			chg->data = NULL;
		}

		*chglist = g_list_append(*chglist, chg);
		len++;
	}

	if (rc != SQLITE_DONE)
		g_critical("sqlite3_step returned %d: %s\n", rc, sqlite3_errmsg(fsess->dbconn));

	sqlite3_finalize(stmt);

	return len;
}

/*****************************************************************************/

GList *five_session_get_changes(FiveSession *fsess, unsigned int *len)
{
	GList *chglist = NULL;
	unsigned int lenlocal = 0;

	assert(fsess != NULL);

	/* TODO: add_changes_* should be robust enough to be used for the slow
	 * sync as well. */
	if (fsess->type == SML_ALERT_REFRESH_FROM_SERVER_BY_SERVER)
	{
		lenlocal += add_dump_artists(fsess, &chglist);
		lenlocal += add_dump_albums(fsess, &chglist);
		lenlocal += add_dump_songs(fsess, &chglist);
		lenlocal += add_dump_playlists(fsess, &chglist);
	}
	else
	{
		lenlocal += add_changes_artists(fsess, &chglist, fsess->server.last);
		lenlocal += add_changes_albums(fsess, &chglist, fsess->server.last);
		lenlocal += add_changes_songs(fsess, &chglist, fsess->server.last);
		lenlocal += add_changes_playlists(fsess, &chglist, fsess->server.last);
	}

	if (len != NULL)
		*len = lenlocal;

	return chglist;
}

void five_change_free(FiveChange *chg)
{
	assert(chg != NULL);

	g_free(chg->data);
	g_free(chg);
}
